﻿using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace BookDemo.S07.S710
{
    class S710 : IBookDemo
    {
        public void Main(string[] args)
        {
            var foobar1 = new FoobarOptions(1, 1);
            var foobar2 = new FoobarOptions(2, 2);
            var foobar3 = new FoobarOptions(3, 3);

            var options = new ServiceCollection()
                .AddOptions()
                .Configure<FakeOptions>("fakeoptions.json")
                .BuildServiceProvider()
                .GetRequiredService<IOptions<FakeOptions>>()
                .Value;

            Debug.Assert(options.Foobar.Equals(foobar1));

            Debug.Assert(options.Array[0].Equals(foobar1));
            Debug.Assert(options.Array[1].Equals(foobar2));
            Debug.Assert(options.Array[2].Equals(foobar3));

            Debug.Assert(options.List[0].Equals(foobar1));
            Debug.Assert(options.List[1].Equals(foobar2));
            Debug.Assert(options.List[2].Equals(foobar3));

            Debug.Assert(options.Dictionary["1"].Equals(foobar1));
            Debug.Assert(options.Dictionary["2"].Equals(foobar2));
            Debug.Assert(options.Dictionary["3"].Equals(foobar3));
        }
    }
    public static class ServiceCollectionExtensions
    {
        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string filePath, string basePath = null) where TOptions : class, new()
        => services.Configure<TOptions>(Options.DefaultName, filePath, basePath);

        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, string filePath, string basePath = null) where TOptions : class, new()
        {
            var fileProvider = string.IsNullOrEmpty(basePath)
                ? new PhysicalFileProvider(Directory.GetCurrentDirectory())
                : new PhysicalFileProvider(basePath);

            return services.AddSingleton<IConfigureOptions<TOptions>>(new JsonFileConfigureOptions<TOptions>(name, filePath, fileProvider));
        }
    }
    internal static class Extensions
    {
        public static bool IsDictionary(this Type type)
            => type.IsGenericType && typeof(IDictionary).IsAssignableFrom(type) && type.GetGenericArguments().Length == 2;
        public static bool IsCollection(this Type type)
            => typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string);
        public static bool IsArray(this Type type)
            => typeof(Array).IsAssignableFrom(type);
    }
    public class FakeOptions
    {
        public FoobarOptions Foobar { get; set; }
        public FoobarOptions[] Array { get; set; }
        public IList<FoobarOptions> List { get; set; }
        public IDictionary<string, FoobarOptions> Dictionary { get; set; }
    }
    public class FoobarOptions : IEquatable<FoobarOptions>
    {
        public int Foo { get; set; }
        public int Bar { get; set; }

        public FoobarOptions() { }
        public FoobarOptions(int foo, int bar)
        {
            Foo = foo;
            Bar = bar;
        }

        public override string ToString() => $"Foo:{Foo}, Bar:{Bar}";
        public bool Equals(FoobarOptions other) => this.Foo == other?.Foo && this.Bar == other?.Bar;
    }
    public class JsonFileConfigureOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class, new()
    {
        private readonly IFileProvider _fileProvider;
        private readonly string _path;
        private readonly string _name;

        public JsonFileConfigureOptions(string name, string path, IFileProvider fileProvider)
        {
            _fileProvider = fileProvider;
            _path = path;
            _name = name;
        }

        public void Configure(string name, TOptions options)
        {
            if (name != null && _name != name)
            {
                return;
            }

            byte[] bytes;
            using (var stream = _fileProvider.GetFileInfo(_path).CreateReadStream())
            {
                bytes = new byte[stream.Length];
                stream.Read(bytes, 0, bytes.Length);
            }

            var contents = Encoding.Default.GetString(bytes);
            contents = contents.Substring(contents.IndexOf('{'));
            var newOptions = JsonConvert.DeserializeObject<TOptions>(contents);
            Bind(newOptions, options);
        }

        public void Configure(TOptions options) => Configure(Options.DefaultName, options);

        private void Bind(object from, object to)
        {
            var type = from.GetType();
            if (type.IsDictionary())
            {
                var dest = (IDictionary)to;
                var src = (IDictionary)from;
                foreach (var key in src.Keys)
                {
                    dest.Add(key, src[key]);
                }
                return;
            }

            if (type.IsCollection())
            {
                var dest = (IList)to;
                var src = (IList)from;
                foreach (var item in src)
                {
                    dest.Add(item);
                }
            }

            foreach (var property in type.GetProperties())
            {
                if (property.IsSpecialName || property.GetMethod == null || property.Name == "Item" || property.DeclaringType != type)
                {
                    continue;
                }

                var src = property.GetValue(from);
                var propertyType = src?.GetType() ?? property.PropertyType;

                if ((propertyType.IsValueType || src is string || src == null) && property.SetMethod != null)
                {
                    property.SetValue(to, src);
                    continue;
                }

                var dest = property.GetValue(to);
                if (null != dest && !propertyType.IsArray())
                {
                    Bind(src, dest);
                    continue;
                }

                if (property.SetMethod != null)
                {
                    var destType = propertyType.IsDictionary()
                        ? typeof(Dictionary<,>).MakeGenericType(propertyType.GetGenericArguments())
                        : propertyType.IsArray()
                        ? typeof(List<>).MakeGenericType(propertyType.GetElementType())
                        : propertyType.IsCollection()
                        ? typeof(List<>).MakeGenericType(propertyType.GetGenericArguments())
                        : propertyType;

                    dest = Activator.CreateInstance(destType);
                    Bind(src, dest);

                    if (propertyType.IsArray())
                    {
                        IList list = (IList)dest;
                        dest = Array.CreateInstance(propertyType.GetElementType(),
                            list.Count);
                        list.CopyTo((Array)dest, 0);
                    }
                    property.SetValue(to, src);
                }
            }
        }
    }
}
